package net.wanderinghorse.jsonmessage;

import net.wanderinghorse.json.AbstractJSONable;
import net.wanderinghorse.json.JSONException;
import net.wanderinghorse.json.JSONObject;
import net.wanderinghorse.json.JSONUtil;

import java.io.InputStream;
import java.io.Serializable;
import java.util.Date;
import java.lang.String;

/**
 JSONMessage is the base class of higher-level
 message objects used by the JSONMessage framework.
 This type implements the core behaviours specified
 (or implied) by the framework's message-class
 conventions.

 Much more information can be found on the JSONMessage
 project home page:

    http://code.google.com/p/jsonmessage/


 Code License: Public Domain

  Reminders to self:

 - When/if json.org accepts/integrates my patches/fixes to JSONObject
 and friends, i'll switch back to using their JSON API. Until then,
 we've got an incompatibility if client code wants to use that API.
 It is easy to transfer data between the two by converting the data
 to a JSON string and passing that string to the other API.

 @author Stephan Beal (http://wanderinghorse.net/home/stephan/)
*/
public class JSONMessage extends AbstractJSONable implements Serializable
{
    private static final long serialVersionUID = 1L;
    /**
     * The path to the JSON configuration file used by several of the framework's
     * classes. It is assumed to be packaged together with the library in its
     * jar file, in the same directory as the JSONMessage class.
     */
    public static final String CONFIG_FILE = "/net/wanderinghorse/jsonmessage/JSONMessageConfig.json";
    /**
     * Common framework-level property keys.
     * These must have values conforming to the message
     * properties defined by the JavaScript-side JSONMessage
     * implementation.
     *
     * TODO: possibly consider changing this to an enum.
     */
    public final class CoreKeys
    {
        public static final String id = "id";
        public static final String msgType = "type";
        public static final String timestamp = "timestamp";
        public static final String payload = "payload";
        public static final String options = "options";
        public static final String credentials = "credentials";
        public static final String responseTo = "responseTo";
        public static final String errorType = "errorType";
        public static final String resultText = "resultText";
        public static final String resultCode = "resultCode";
        public static final String errorMessage = "errorMessage";
    }
    public enum CoreKey {
        id("id")
        ;
        private String value;
        private CoreKey(final String v) {
            value = v;
        }
        public String toString() {
            return value;
        }
    }
    /** Internal state for nextID. */
    private static long _nextID = 0;
    /** Internal state for nextID. */
    private static long _prevTime = 0;

    /**
     * Generates a framework-conventional ID string, based on
     * the current time. The ID is only guaranteed to be unique
     * within a certain time frame and JVM session. If the system
     * clock changes (is decreased in value) during the life of this
     * object, it may produce duplicate results during its lifetime
     * (garbage in, garbage out!).
     *
     * Note that there is no "standard" for ID strings. Their structure
     * is largley unimportant, but each ID should be unique within the context
     * of a given application session. They are not intended to be persistent
     * across application sessions.
     *
     * In practice, it doesn't really matter if they are unique, but making
     * them unqique can simplify debugging or tracing the origin of a specific
     * request or response.
     *
     * @param hint If (hint>=0) then it is considered to be a timestamp
     * in (GMT+milliseconds), else the current time is used.
     * @return A JVM-session-unique stringified message ID.
     */
    protected static synchronized String nextID( final long hint )
    {
        Date now = (hint>=0) ? new Date(hint) : new Date();
        long ts = now.getTime();
        ts = (ts - (ts%1000));
        try
        {
            if( ts > _prevTime )
            {
                /**
                    For any given second, keep a running count of the number
                    of requests made during that second, resetting to 0 the
                    first time an ID is generated during any given second.
                    (Yes, Java _is_ capable of producing more than one
                    message per second.) Note that temporary objects also
                    get IDs, so messages generated by the client may not have
                    get IDs which always increment by 1 tick.
                */
                _prevTime = ts;
                _nextID = 0;
            }
            return String.format( "%1$tY%1$tm%1$td%1$tH%1$tM%1$tS/%2$08d",
                                    now, ++_nextID );
        }
        catch( Exception e )
        {
            return "JSONMessage.nextID(): ERROR FORMATTING ID STRING: "+e;
        }
    }

    /**
     * A JSONable helper type for encapsulating message credentials.
     */
    public class Credentials extends AbstractJSONable implements Serializable
    {
        public Credentials() {
        }
        public Credentials( String name, String token ) {
            this.name = name;
            this.token = token;
        }
        private String name;
        private String token;

        public String getName() {
            return name;
        }

        public Credentials setName(String name) {
            this.name = name;
            return this;
        }

        public String getToken() {
            return token;
        }

        public Credentials setToken(String token) {
            this.token = token;
            return this;
        }

        @Override
        public JSONObject toJSONObject() throws JSONException {
            return new JSONObject(this);
        }

        @Override
        public void fromJSONObject(JSONObject src) throws JSONException {
            this.name = src.optString("name", null );
            this.token = src.optString("token", null );
        }
    }
    /**
     * Core message properties, as specified by the framework's
     * conventions. This class is for internal use only by JSONMessage
     * and its subclasses.
     */
    protected class CoreProps extends AbstractJSONable implements Serializable
    {
        protected String id = null;
        /**
         * (GMT + milliseconds)
         */
        protected long timestamp = 0;
        protected String msgType = null;
        protected Object options = null;
        protected Object payload = null;
        protected Credentials credentials = null;
        protected CoreProps()
        {
            this.timestamp = java.lang.System.currentTimeMillis();
            this.id = JSONMessage.nextID( this.timestamp );
        }

        /**
         * See fromJSONObject(JSONObject).
         * @param src The input state.
         * @throws JSONException If the underlying JSONObject API throws.
         */
        protected CoreProps( JSONObject src ) throws JSONException
        {
            this.fromJSONObject( src );
        }

        /**
         * Populates this object from src.
         * @param src The input state.
         * @throws JSONException If the underlying JSONObject API throws.
         */
        @Override
        public void fromJSONObject( JSONObject src ) throws JSONException
        {
            this.fromJSONObject( src, null );
        }
        /**
         * Identical to fromJSON(JSONObject), but if src is an outer-most
         * wrapper object then it is stripped and proxy.setWrapperName()
         * is set to that wrapper object's key/name.
         *
         * @param src The input state.
         * @param proxy The message on which to call setWrapperName(). May be null.
         * @throws JSONException If the underlying JSONObject API throws.
         * @return A bit of a kludge: if src is an outer wrapper object, the inner object
         * is returned, otherwise src is returned. 
         */
        public JSONObject fromJSONObject( final JSONObject src, JSONMessage proxy ) throws JSONException
        {
            JSONObject real;
            if( (1 == src.length())
                && !src.has(CoreKeys.id)
                && !src.has(CoreKeys.msgType)
                && !src.has(CoreKeys.timestamp)
            )
            {
                /**
                 Framework-wide kludge to strip away the outer-most wrapper object
                 if src appears to be a message wrapped in such a beast.
                 */
                String key = src.keys().next().toString();
                Object check = src.get(key);
                if( check instanceof JSONObject )
                {
                    real = (JSONObject)check;
                    if( null != proxy ) {
                        proxy.setWrapperName( key );
                    }
                }
                else
                {
                    throw new JSONException("JSON outer wrapper object does not contain a JSONObject!");
                }
            }
            else {
                real = src;
            }
            this.timestamp = real.optLong( CoreKeys.timestamp, 0 );
            this.id = real.optString( CoreKeys.id, null );
            this.msgType = real.optString( CoreKeys.msgType, this.msgType );
            this.options = real.opt( CoreKeys.options );
            this.payload = real.opt( CoreKeys.payload );
            {
                JSONObject cr = real.optJSONObject( CoreKeys.credentials );
                if( null != cr ) {
                    this.credentials = new Credentials();
                    this.credentials.fromJSONObject( cr );
                }
                else {
                    this.credentials = null;
                }
            }
            return real;
        }
        @Override
        public JSONObject toJSONObject() throws JSONException {
            return new JSONObject(this);
        }
        /** Bean-style getter.
         * @return this.id
         */
        public final String getId() { return this.id; }
        /** Bean-style getter.
         * @return this.timestamp
         */
        public final long getTimestamp() { return this.timestamp; }
        /** Bean-style getter.
         * @return this.msgType
         */
        public final String getType() { return this.msgType; }
        /** Bean-style getter.
         * @return this.options
         */
        public final Object getOptions() { return this.options; }
        /** Bean-style getter.
         * @return this.payload
         */
        public final Object getPayload() { return this.payload; }
        /** Bean-style getter.
         * @return this.credentials
         */
        public final Credentials getCredentials() { return this.credentials; }
    }

    /**
     * Default string used for the getWrapperName() value.
     */
    public static final String defaultWrapperName = "JSONMessage";

    /**
        Part of the internal state - is not officially part
        of the message content, but is used as the name of the
        framework-conventional JSON wrapper object for JSON-ized
        messages.
     */
    private String wrapperName = defaultWrapperName;

    /**
     * This message's core properties.
     */
    protected CoreProps coreProps = new CoreProps();

    /**
     * Sets the id and type fields to default values.
     */
    public JSONMessage()
    {
    }

    /**
     * It is sometimes necessary/desired to ensure that the message's
     * type name and wrapper name are set before importing arbitrary
     * JSON input, to ensure that these two fields get non-null values. 
     * @param typeName The value for getType()
     * @param wrapName The value for getWrapperName()
     */
    protected JSONMessage( String typeName, String wrapName )
    {
        this.coreProps.msgType = typeName;
        this.wrapperName = wrapName;
    }

    /**
     * See fromJSON(String).
     *
     * @param json A JSON-format JSONMessage object.
     * @throws JSONException If JSON parsing fails or the data is not
     * recognized as message data.
     */
    public JSONMessage( String json ) throws JSONException
    {
        this.fromJSON( json );
    }

    /**
     * See fromJSONObject().
     *
     * @param json A JSON-structured JSONMessage object.
     * @throws JSONException If the object is not
     * recognized as message data.
     */
    public JSONMessage( JSONObject json ) throws JSONException
    {
        this.fromJSONObject( json );
    }

    /**
     * Assumes that src holds a JSONMessage object
     * structured in the framework's conventional manner.
     *
     * After decoding the core properties, the protected member
     * doFromJSONObject() is called to handle any subclass-specific
     * properties.
     *
     * @param src Encoded message data.
     * @throws JSONException If the object is not
     * recognized as message data.
     * @throws NullPointerException if src is null.
     */
    public final void fromJSONObject( final JSONObject src ) throws JSONException
    {
        if( null == src )
        {
            throw new java.lang.NullPointerException("fromJSON() may not be passed null!");
        }
        this.coreProps = new CoreProps();
        this.doFromJSONObject(this.coreProps.fromJSONObject( src, this ));
    }

    /**
     * May optionally be implemented in order to extend the properties handled by
     * fromJSONObject(). This is called by that function once the core properties
     * are handled.
     *
     * If the input JSON passed to fromJSON() or fromJSONObject() contains an outer
     * wrapper object, then it is discarded before this is called. Thus the object
     * passed here "should" be the JSON representation of a Response object.
     *
     * @param src The input JSON object.
     * @throws JSONException If the underlying JSONObject API throws.
     */
    protected void doFromJSONObject( JSONObject src ) throws JSONException {
    }
       

    /**
     * Creates a new JSONObject which which contains this message's content
     * using a framework-standard structure (documented in detail elsewhere
     * - try the library manual or the project wiki).
     *
     * @param includeWrapper If true, the returned object contains an outer wrapper object.
     * Framework-wide, the default value for this is true.
     * @return The JSON representation of this object.
     * @throws JSONException If the underlying JSONObject API throws.
     */
    public final JSONObject toJSONObject( boolean includeWrapper ) throws JSONException
    {
        JSONObject jo = this.coreProps.toJSONObject();
        this.toJSONObject( jo );
        if( includeWrapper )
        {
            JSONObject root = new JSONObject();
            root.put( this.getWrapperName(), jo );
            return root;
        }
        else
        {
            return jo;
        }
    }

    /**
     * May optionally be re-implemented to add custom message properties during
     * the toJSON() process. This is called by toJSONObject(boolean) after
     * the core properties have been JSON-ized.
     *
     * The default implementation does nothing, and need not be called by
     * subclass implementations.
     *
     * @param dest Current message state, which has (by the time this is called)
     * already been initialized to contain this object's core message properties.
     * Certain properties may legally be modified by subclasses, but most
     * should not. Primarily (id, type, responseTo) should not be modified,
     * and doing so might result in undefined behaviour down-stream.
     * @throws JSONException if any of the underlying JSONObject API throws.
     */
    protected void toJSONObject( JSONObject dest ) throws JSONException
    {
    }

    /**
     * Equivalent to this.toJSONObject( false ).
     * @return See toJSONObject(JSONObject).
     * @throws JSONException If the underlying JSONObject API throws.
     */
    @Override
    public final JSONObject toJSONObject() throws JSONException
    {
        return this.toJSONObject( false );
    }

    /**
     * @return This message's timestamp. Normally this is when it was created,
     * but it may be re-set when fromJSON() or fromJSONObject() import external data.
     */
    public final long getTimestamp()
    {
        return this.coreProps.timestamp;
    }

    /**
     * @param ts The message typestamp, in (GMT+milliseconds).
     * @return this object.
     */
    public final JSONMessage setTimestamp( long ts )
    {
        this.coreProps.timestamp = ts;
        return this;
    }

    /**
     * @return This object's message ID, which may be null (but a null ID
     * is not semantically legal for message transport purposes).
     */
    public final String getId()
    {
        return this.coreProps.id;
    }

    /**
     * @param id Message ID. The exact format is unspecified, and clients
     * may use whatever they wish, but it should be unique per application
     * session (or globally unique, if used in a cross-server environment).
     * Message IDs are intended to be transient, and not used as (e.g.)
     * keys for database entries..
     * @return this object.
     */
    public final JSONMessage setId( String id )
    {
        this.coreProps.id = (id==null) ? nextID(-1) : id;
        return this;
    }

    /**
     * @return This message's type string.
     */
    public final String getType()
    {
        return this.coreProps.msgType;
    }

    /**
     * Sets the message type name.
     * @param t Message type name.
     * @return this object.
     */
    public final JSONMessage setType( String t )
    {
        this.coreProps.msgType = t;
        return this;
    }

    /**
     * @return The payload object, which may be null.
     */
    public final Object getPayload()
    {
        return this.coreProps.payload;
    }

    /**
     * Sets the message payload, which must be one of the following types:
     *
     * Boolean/boolean, Double/double, Integer/integer, Long/long,
     * JSONArray, JSONObject, String.
     *
     * Or one of these special-case values:
     *
     * null or the JSONObject.NULL object.
     *
     * If it is not one of those, then subsequent toJSON-ing will likely
     * throw.
     *
     * Setting the payload to a literal null will exclude the payload
     * from the JSON form of the message, whereas JSONObject.NULL will
     * insert a JavaScript/JSON null value into the output. The semantics
     * of both cases are, for purposes of handling the payload, identical.
     *
     * The type/value limitations are inherited via the JSONObject API, and
     * are intended to cover types which have JSON-native equivalents.
     *
     * @param val Payload value.
     * @return this object.
     */
    public final JSONMessage setPayload( Object val )
    {
        this.coreProps.payload = val;
        return this;
    }

    /**
     * @return Value previously passed to setOptions(), or null
     * if no value has been set.
     */
    public final Object getOptions()
    {
        return this.coreProps.options;
    }

    /**
     * Sets message-type-specific low-level options. This is reserved
     * for message-specific metadata which does not semantically belong
     * in the payload.
     *
     * In practice, this field is never used, but exists because there are foreseeable
     * cases for using it. e.g. a Response class might need to support several different
     * payload versions for backwards compatibility, and the version number could be
     * stored here. It can also be used for backend-specific metadata which does
     * not belong in the payload. In such cases, it is important that the client-side
     * message-sending code populate the options object appropriately.
     *
     * @param val A value, with the same type restrictions as setPayload().
     * @return This object.
     */
    public final JSONMessage setOptions( Object val )
    {
        this.coreProps.options = val;
        return this;
    }

    /**
     * @return This message's "wrapper name", used as the name for the
     * outer-most wrapper object when toJSON(true) is called.
     */
    public final String getWrapperName()
    {
        return this.wrapperName;
    }

    /**
     * Sets the name used for the framework-conventional outer wrapper
     * object used when to-JSONing a message. This string is primarily
     * of informational value on, and is not intended to be used as
     * a lookup key or something similar.
     *
     * @param val Name to use for outer-most wrapper object when
     * toJSON(true) is called. If passed null, some non-empty (but
     * unspecified, and not guaranteed to be stable across
     * invocations) value is used.
     * @return This object.
     */
    public final JSONMessage setWrapperName( String val )
    {
        this.wrapperName = (val==null) ? this.getClass().getName() : val;
        return this;
    }

    /**
     * @return Message credentials, or null if none have been set.
     */
    public final Credentials getCredentials()
    {
        return this.coreProps.credentials;
    }

    /**
     * Sets framework-level message credentials data.
     *
     * @param val Credentials object for this message, which may be null.
     * @return This object.
     */
    public final JSONMessage setCredentials( Credentials val )
    {
        this.coreProps.credentials = val;
        return this;
    }

    /**
     * Convenience overload. If both name and token are null,
     * the semantics are the same as calling setCredentials((Credentials)null).
     *
     * @param name Credentials user name.
     * @param token Credentials password/token.
     * @return This object.
     */
    public final JSONMessage setCredentials( String name, String token )
    {
         this.coreProps.credentials = ((null!=name) && (null!=token))
            ? new Credentials(name, token)
            : null ;
         return this;
    }

    private static JSONObject _config;

    /**
     * Tries to load the file named CONFIG_FILE as JSON. The purpose of
     * that file is for holding framework-level configuration data.
     *
     * If loading succeeds, the shared copy is kept in memory. However,
     * changes made to the returned object only survive until the end of the
     * application - the changes are not saved back to the original source.
     *
     * @return A shared copy of the config object, or null if loading fails.
     */
    public static JSONObject getConfig()
    {
        synchronized (JSONMessage.class) {
            if( null == _config ) {
                try {
                    InputStream is =
                     //getClass().getClassLoader()
                    JSONMessage.class
                    //Thread.currentThread().getContextClassLoader()
                    .getResourceAsStream( JSONMessage.CONFIG_FILE);
                    if( null != is ) {
                        _config = JSONUtil.readAllAsJSON( is );
                    }
                }
                catch(Exception ex) {
                    /** ignore */
                    //System.out.println("WARNING: could not read JSONMessage config: "+ex);
                }
            }
        }
        return _config;
    }

    private final static int DEFAULT_INDENTATION = 4;
    public static int getDefaultIndentation() {
        JSONObject jo = getConfig();
        return (null==jo)
                ? DEFAULT_INDENTATION
                : jo.optInt( "indentation", DEFAULT_INDENTATION );
    }
}
